from collections import defaultdict

from utils.interval import Interval
from utils.misc import MAXFLOAT

from cranesched.instance.instance import Instance
from cranesched.instance.activity import Activity
from cranesched.instance.precedences import PrecedenceGraph
from cranesched.instance.warehouse.crane import Crane
from cranesched.instance.warehouse.warehouse import Warehouse


def sp2cs_instance(sp_sol, action_duration=0.0, place_coords=None, initial_time=0.0,
                   crane_capacity=None, crane_position=None, crane_speed=None, crane_size=1.0):
    prec_graph = sp2cs_precgraph(sp_sol, action_duration, place_coords, initial_time)
    warehouse = sp2cs_warehouse(sp_sol, crane_capacity, crane_position, crane_speed, crane_size)
    instance = Instance(prec_graph, warehouse)
    instance.determine_warehouse_limits()
    return instance
    
    
def sp2cs_precgraph(sp_sol, action_duration=0.0, place_coords=None, 
                    initial_time=0.0, verbose=None):
    """Create a precedence graph from a given list of SP actions and items. Additional data 
    required for the creation of the precedence graph includes the duration of each action and 
    the coordinates of each place. Initial time determines the time of the event (i.e. the start 
    of a release or delivery interval).
        sp_items - [stacking.Item]
        sp_actions - [stacking.Action]
        action_duration - {action_id: float} - default=0.0 if missing
        place_coords - {place_id: (x, y)} - retrieved from action metadata if missing
        initial_time - float - the date of the first event. Cranes always start at t=0.0, so 
            setting initial time to 30.0 means that the cranes will have 30 seconds to move freely 
            before the first event.
    """
    sp_items = sp_sol.instance.items.values()
    sp_warehouse = sp_sol.instance.warehouse
    sp_places = sp_sol.instance.warehouse.places
    sp_actions = sp_sol.actions
    action_duration = _check_dict_arg(action_duration, sp_actions, None)
    place_coords = _check_dict_arg(place_coords, sp_places, (lambda p: p.meta.xy), False)
    if initial_time is not None:
        sp_items = _shift_initial_time_to(sp_items, initial_time)
    item_map = dict((item.id, item) for item in sp_items)
    prec_graph = PrecedenceGraph()
    for action in sp_actions:
        x, y = place_coords[sp_warehouse[action.place]]
        duration = action_duration[action]
        window = _compute_window(action, item_map)
        prec_graph.append(Activity(action, x, y, duration, window))
    prec_graph.setup_precedences()
    prec_graph.preprocess(verbose=verbose)
    return prec_graph
    
    
def sp2cs_warehouse(sp_sol, crane_capacity=None, crane_position=None, 
                    crane_speed=None, crane_size=1.0):
    sp_cranes = sp_sol.instance.warehouse.cranes
    crane_capacity = _check_dict_arg(crane_capacity, sp_cranes, lambda c: c.meta.capacity)
    crane_position = _check_dict_arg(crane_position, sp_cranes, lambda c: c.meta.initial_xy)
    crane_speed = _check_dict_arg(crane_speed, sp_cranes, lambda c: c.meta.speed)
    cs_cranes = []
    x = 0.0
    for c in sorted(sp_cranes, key=lambda c: c.id):
        _, y = crane_position[c]
        xspeed, yspeed = crane_speed[c]
        cs_cranes.append(Crane(id=c.id, capacity=crane_capacity[c], 
                               items=[i.id for i in c.items], time=0.0, 
                               x=x, xspeed=xspeed, xsize=crane_size, 
                               y=y, yspeed=yspeed, ysize=crane_size))
        x += crane_size
    return Warehouse(cranes=cs_cranes)
    
    
def _check_dict_arg(arg, keys, getter, allow_shared_value=True):
    """Auxiliary function. This is used for arguments which specify crane or action attributes, and 
    the argument can be:
        1) a dictionary - specifying an individual value for each key.
        2) None - in which case a default getter function is used on the key to generate the 
            corresponding value.
        3) any other value - specifying a constant value that is shared by all keys.
    The function returns a dictionary that associates each key to a value."""
    if isinstance(arg, dict):
        return arg
    if arg is None:
        return dict((k, getter(k)) for k in keys)
    if allow_shared_value:
        return defaultdict(lambda: arg)
    raise Exception("invalid dictionary argument")
    
    
def _shift_initial_time_to(items, start):
    """Creates copies of all the items. The release and due dates (intervals) of the copies are 
    then shifted so that the earliest event (either a release or delivery) occurs at 't=start'."""
    items = [item.deepcopy() for item in items]
    all_times = [i.release.start if i.release is not None else i.due.start 
                 for i in items if i.release is not None or i.due is not None]
    if len(all_times) == 0:
        return items
    delta = start - min(all_times)
    for item in items:
        if item.release is not None:
            item.release.shift(delta)
        if item.due is not None:
            item.due.shift(delta)
    return items
    
    
def _compute_window(action, item_map):
    """Computes the "satisfiability window" for an action. This is the time interval during 
    which the action **must** be satisfied."""
    if action.is_release:
        attr1 = attr2 = "release"
    elif action.is_delivery:
        attr1 = attr2 = "due"
    else:
        attr1, attr2 = "release", "due"
        
    start, end = -MAXFLOAT, +MAXFLOAT
    for item_id in action.items:
        item = item_map[item_id]
        interval1 = getattr(item, attr1)
        interval2 = getattr(item, attr2)
        if interval1 is not None:
            start = max(start, interval1.start)
        if interval2 is not None:
            end = min(end, interval2.end)
    return Interval(start, end)
    
    
Instance.from_sp = staticmethod(sp2cs_instance)
PrecedenceGraph.from_sp = staticmethod(sp2cs_precgraph)
Warehouse.from_sp = staticmethod(sp2cs_warehouse)
